/*!
* jQuery Data Link plugin 1.0.0pre
* http://github.com/jquery/jquery-datalink
*
* Copyright Software Freedom Conservancy, Inc.
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*/
(function ($, undefined) {

	var oldcleandata = $.cleanData,
	links = [],
	fnSetters = {
		val: "val",
		html: "html",
		text: "text"
	},
	eventNameSetField = "setField",
	eventNameChangeField = "changeField";

	function getLinks(obj) {
		var data = $.data(obj),
		cache,
		fn = data._getLinks || (cache = { s: [], t: [] }, data._getLinks = function () { return cache; });
		return fn();
	}

	function bind(obj, wrapped, handler) {
		wrapped.bind(obj.nodeType ? "change" : eventNameChangeField, handler);
	}
	function unbind(obj, wrapped, handler) {
		wrapped.unbind(obj.nodeType ? "change" : eventNameChangeField, handler);
	}

	$.extend({
		cleanData: function (elems) {
			for (var j, i = 0, elem; (elem = elems[i]) != null; i++) {
				// remove any links with this element as the source
				// or the target.
				var links = $.data(elem, "_getLinks");
				if (links) {
					links = links();
					// links this element is the source of
					var self = $(elem);
					$.each(links.s, function () {
						unbind(elem, self, this.handler);
						if (this.handlerRev) {
							unbind(this.target, $(this.target), this.handlerRev);
						}
					});
					// links this element is the target of
					$.each(links.t, function () {
						unbind(this.source, $(this.source), this.handler);
						if (this.handlerRev) {
							unbind(elem, self, this.handlerRev);
						}
					});
					links.s = [];
					links.t = [];
				}
			}
			oldcleandata(elems);
		},
		convertFn: {
			"!": function (value) {
				return !value;
			}
		},
		setField: function (target, field, value) {
			if (target.nodeType) {
				var setter = fnSetters[field] || "attr";
				$(target)[setter](value);
			} else {
				var parts = field.split(".");
				parts[1] = parts[1] ? "." + parts[1] : "";

				var $this = $(target),
					args = [parts[0], value];

				$this.triggerHandler(eventNameSetField + parts[1] + "!", args);
				if (value !== undefined) {
					target[field] = value;
				}
				$this.triggerHandler(eventNameChangeField + parts[1] + "!", args);
			}
		}
	});

	function getMapping(ev, changed, newvalue, map) {
		var target = ev.target,
		isSetData = ev.type === eventNameChangeField,
		mappedName,
		convert,
		name;
		if (isSetData) {
			name = changed;
			if (ev.namespace) {
				name += "." + ev.namespace;
			}
		} else {
			name = (target.name || target.id);
		}

		if (!map) {
			mappedName = name;
		} else {
			var m = map[name];
			if (!m) {
				return null;
			}
			mappedName = m.name;
			convert = m.convert;
			if (typeof convert === "string") {
				convert = $.convertFn[convert];
			}
		}
		return {
			name: mappedName,
			convert: convert,
			value: isSetData ? newvalue : $(target).val()
		};
	}

	$.extend($.fn, {
		link: function (target, mapping) {
			var self = this;
			if (!target) {
				return self;
			}
			function matchByName(name) {
				var selector = "[name=" + name + "], [id=" + name + "]";
				// include elements in this set that match as well a child matches
				return self.filter(selector).add(self.find(selector));
			}
			if (typeof target === "string") {
				target = $(target, this.context || null)[0];
			}
			var hasTwoWay = !mapping,
			map,
			mapRev,
			handler = function (ev, changed, newvalue) {
				// a dom element change event occurred, update the target
				var m = getMapping(ev, changed, newvalue, map);
				if (m) {
					var name = m.name,
						value = m.value,
						convert = m.convert;
					if (convert) {
						value = convert(value, ev.target, target);
					}
					if (value !== undefined) {
						$.setField(target, name, value);
					}
				}
			},
			handlerRev = function (ev, changed, newvalue) {
				// a change or changeData event occurred on the target,
				// update the corresponding source elements
				var m = getMapping(ev, changed, newvalue, mapRev);
				if (m) {
					var name = m.name,
						value = m.value,
						convert = m.convert;
					// find elements within the original selector
					// that have the same name or id as the field that updated
					matchByName(name).each(function () {
						newvalue = value;
						if (convert) {
							newvalue = convert(newvalue, target, this);
						}
						if (newvalue !== undefined) {
							$.setField(this, "val", newvalue);
						}
					});
				}

			};
			if (mapping) {
				$.each(mapping, function (n, v) {
					var name = v,
					convert,
					convertBack,
					twoWay;
					if ($.isPlainObject(v)) {
						name = v.name || n;
						convert = v.convert;
						convertBack = v.convertBack;
						twoWay = v.twoWay !== false;
						hasTwoWay |= twoWay;
					} else {
						hasTwoWay = twoWay = true;
					}
					if (twoWay) {
						mapRev = mapRev || {};
						mapRev[n] = {
							name: name,
							convert: convertBack
						};
					}
					map = map || {};
					map[name] = { name: n, convert: convert, twoWay: twoWay };
				});
			}

			// associate the link with each source and target so it can be
			// removed automaticaly when _either_ side is removed.
			self.each(function () {
				bind(this, $(this), handler);
				var link = {
					handler: handler,
					handlerRev: hasTwoWay ? handlerRev : null,
					target: target,
					source: this
				};
				getLinks(this).s.push(link);
				if (target.nodeType) {
					getLinks(target).t.push(link);
				}
			});
			if (hasTwoWay) {
				bind(target, $(target), handlerRev);
			}
			return self;
		},
		unlink: function (target) {
			this.each(function () {
				var self = $(this),
				links = getLinks(this).s;
				for (var i = links.length - 1; i >= 0; i--) {
					var link = links[i];
					if (link.target === target) {
						// unbind the handlers
						//wrapped.unbind( obj.nodeType ? "change" : "changeData", handler );
						unbind(this, self, link.handler);
						if (link.handlerRev) {
							unbind(link.target, $(link.target), link.handlerRev);
						}
						// remove from source links
						links.splice(i, 1);
						// remove from target links
						var targetLinks = getLinks(link.target).t,
						index = $.inArray(link, targetLinks);
						if (index !== -1) {
							targetLinks.splice(index, 1);
						}
					}
				}
			});
		},
		setField: function (field, value) {
			return this.each(function () {
				$.setField(this, field, value);
			});
		}
	});

})(jQuery);